\section{PyTotch教程}

\subsection{安装pytorch}
在安装Pytorch之前，我们需要检查系统信息：
\begin{lstlisting}
# 检查 Windows 版本
winver

# 检查 Python 版本
python --version

# 检查是否有 NVIDIA GPU
nvidia-smi
\end{lstlisting}

结果如图\ref{准备工作}
\begin{figure}[htb]
	\centering
	\includegraphics[width=\linewidth]{assets/images/image1.png}
	\caption{准备工作}
	\label{准备工作}
\end{figure}

然后安装Python环境管理：
\begin{lstlisting}
# 下载并安装 Miniconda
# Windows: https://repo.anaconda.com/miniconda/Miniconda3-latest-Windows-x86_64.exe
# macOS: https://repo.anaconda.com/miniconda/Miniconda3-latest-MacOSX-x86_64.sh
# Linux: https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh

# 创建专用环境
conda create -n pytorch_env python=3.10
conda activate pytorch_env
\end{lstlisting}

使用venv
\begin{lstlisting}
# 创建虚拟环境
python -m venv pytorch_env

# 激活环境
# Windows
pytorch_env\Scripts\activate
# macOS/Linux
source pytorch_env/bin/activate
\end{lstlisting}

安装pytorch
\begin{lstlisting}
# 最新稳定版本
pip install torch torchvision torchaudio

# 指定版本
pip install torch==2.1.0 torchvision==0.16.0 torchaudio==2.1.0

# 仅 CPU 版本（更小的安装包）
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
\end{lstlisting}

具体操作如图\ref{安装pytorch}和图\ref{安装pytorch结果}
\begin{figure}[H]
	\centering
	\includegraphics[width=\linewidth]{assets/images/image2.png}
	\caption{安装pytorch}
	\label{安装pytorch}
\end{figure}

\begin{figure}[H]
	\centering
	\includegraphics[width=\linewidth]{assets/images/结果.png}
	\caption{安装pytorch结果}
	\label{安装pytorch结果}
\end{figure}

\subsection{张量}

我的学习首先从理解张量的定义开始。PyTorch 张量类似于 NumPy 的 `ndarray`，但其一大优势是可以在 GPU 上加速计算。我了解到张量可以有不同的维度（标量、向量、矩阵、更高维的张量），并且每个张量都有其数据类型（`dtype`）和设备（`device`）。

在创建张量方面，我实践了多种方法，包括从数据直接创建、使用 NumPy 数组转换，以及使用 PyTorch 的内置函数创建特定类型的张量。

\begin{verbatim}
import torch
import numpy as np

# 从数据直接创建张量
data = [[1, 2],[3, 4]]
x_data = torch.tensor(data)
print(f"Directly from data:\n{x_data}")

# 从NumPy数组创建张量
np_array = np.array(data)
x_np = torch.from_numpy(np_array)
print(f"From NumPy array:\n{x_np}")

# 创建全1或全0张量
x_ones = torch.ones_like(x_data) # 保留 x_data 的属性
print(f"Ones Tensor:\n{x_ones}")
x_rand = torch.rand_like(x_data, dtype=torch.float) # 覆盖 x_data 的数据类型
print(f"Random Tensor:\n{x_rand}")

# 创建随机或常量张量
shape = (2,3,)
rand_tensor = torch.rand(shape)
ones_tensor = torch.ones(shape)
zeros_tensor = torch.zeros(shape)
print(f"Random Tensor:\n{rand_tensor}\n")
print(f"Ones Tensor:\n{ones_tensor}\n")
print(f"Zeros Tensor:\n{zeros_tensor}")
\end{verbatim}

结果如图\ref{张量的创建}
\begin{figure}[H]
	\centering
	\includegraphics[width=\linewidth]{assets/images/张亮1.png}
	\caption{张量的创建}
	\label{张量的创建}
\end{figure}

通过这些练习，我清楚地看到了不同创建方法的输出，并理解了 `dtype` 和 `device` 的重要性。特别是在创建随机张量时，我注意到默认的数据类型是 `torch.float32`。

在掌握了张量创建之后，我开始探索张量的各种属性，这些属性对于理解张量的结构和进行后续操作至关重要。我关注了 `shape`、`dtype` 和 `device` 这三个核心属性。

\begin{verbatim}
tensor = torch.ones(4, 4)
print(f"Shape of tensor: {tensor.shape}")
print(f"Dtype of tensor: {tensor.dtype}")
print(f"Device tensor is stored on: {tensor.device}")
\end{verbatim}

结果如图\ref{张量类型}
\begin{figure}[H]
	\centering
	\includegraphics[width=\linewidth]{assets/images/张亮类型.png}
	\caption{张量类型}
	\label{张量类型}
\end{figure}

这些属性输出简洁明了，让我能够快速获取张量的基本信息。理解张量的 `device` 尤其重要，因为这决定了张量是在 CPU 还是 GPU 上进行计算。

张量操作是 PyTorch 的核心功能之一。我尝试了多种操作，包括标准的算术运算、矩阵乘法以及灵活的索引和切片。我发现 PyTorch 的操作语法与 NumPy 非常相似，这使得学习曲线相对平缓。

\begin{verbatim}
# 张量在CPU上
tensor = torch.ones(4, 4)

# 索引
print(f"First row: {tensor[0]}")
print(f"First column: {tensor[:, 0]}")
print(f"Last column: {tensor[..., -1]}")
tensor[:,1] = 0 # 将第二列所有元素赋值为0
print(tensor)

# 张量拼接
t1 = torch.cat([tensor, tensor, tensor], dim=1) # 沿维度1拼接
print(f"Concatenated tensor:\n{t1}")

# 张量乘法
tensor = torch.ones(4, 4)
tensor_mul = tensor.mul(tensor) # 元素级乘法
print(f"Element-wise multiplication:\n{tensor_mul}")
tensor_matmul = tensor.matmul(tensor.T) # 矩阵乘法，注意转置
print(f"Matrix multiplication:\n{tensor_matmul}")

# 就地操作
print(f"Original tensor:\n{tensor}")
tensor.add_(5) # 就地加5
print(f"Tensor after in-place add:\n{tensor}")
\end{verbatim}

结果如图\ref{张量操作}

\begin{figure}[H]
	\centering
	\includegraphics[width=\linewidth]{assets/images/张量操作.png}
	\caption{张量操作}
	\label{张量操作}
\end{figure}

在 PyTorch 和其他科学计算库（如 NumPy）之间无缝切换的能力非常重要。我学习了如何将张量转换为 NumPy 数组，以及如何将 NumPy 数组转换为张量。

\begin{verbatim}
# Tensor to NumPy
np_array = tensor.numpy()
print(f"Tensor to NumPy:\n{np_array}")
print(f"Type of np_array: {type(np_array)}")

# NumPy to Tensor
new_tensor = torch.from_numpy(np_array)
print(f"NumPy to Tensor:\n{new_tensor}")
print(f"Type of new_tensor: {type(new_tensor)}")
\end{verbatim}

我发现，当张量在 CPU 上时，它们与 NumPy 数组共享底层内存，这意味着对其中一个的修改会反映在另一个上。这是一个非常高效的特性，但同时也需要在使用时注意，以避免意外的副作用。

\begin{verbatim}
# 张量在CPU上时，它们和NumPy数组共享底层内存
tensor.add_(1) # 修改张量
print(f"Tensor after add_:\n{tensor}")
print(f"NumPy array after tensor modification:\n{np_array}") # np_array也会被修改
\end{verbatim}

\subsection{PyTorch 神经网络基础学习}

在本次实验中，我首先学习了神经网络的基本概念。我了解到，神经网络是模仿人脑处理信息方式的计算模型，其最基本的单元是\textbf{神经元 (Neuron)}。每个神经元接收多个输入信号，通过加权求和、加上偏置项(Bias)，最后经过一个激活函数(Activation Function)处理后产生输出。这个过程可以用以下数学公式来表示：
\[ \text{Output} = f\left(\sum_{j} w_j x_j + \text{Bias}\right) \]
其中，$w_j$ 是权重，$x_j$ 是输入，而 $f$ 是激活函数。

多个神经元按层级结构组织起来，就构成了神经网络。一个典型的网络结构包括\textbf{输入层 (Input Layer)}、一个或多个\textbf{隐藏层 (Hidden Layer)} 和\textbf{输出层 (Output Layer)}。数据从输入层开始，逐层向前传播，直到输出层，这种网络被称为前馈神经网络 (Feedforward Neural Network)。

在理解了这些基本理论后，我开始使用 PyTorch 来构建我的第一个神经网络。PyTorch 中构建神经网络的核心是 \texttt{torch.nn} 模块。任何自定义的网络模型都需要继承 \texttt{nn.Module} 类，并实现其 \texttt{\_\_init\_\_()} 和 \texttt{forward()} 两个方法。

\begin{itemize}
    \item \texttt{\_\_init\_\_()}：用于定义网络中需要用到的所有层，例如全连接层(\texttt{nn.Linear})、卷积层等。
    \item \texttt{forward()}：用于定义数据在网络中的前向传播路径，即数据如何从输入层流经各个层最终到达输出层。
\end{itemize}

下面是我根据学习资料编写的第一个简单的全连接网络模型。这个网络包含一个输入层、一个隐藏层和一个输出层。

\textbf{代码 1：定义一个简单的神经网络模型}
\begin{verbatim}
import torch
import torch.nn as nn

# 定义一个简单的神经网络模型
class SimpleNN(nn.Module):
    def __init__(self):
        # 调用父类的构造函数
        super(SimpleNN, self).__init__()
        # 定义一个输入层到隐藏层的全连接层
        # 输入 2 个特征，输出 2 个特征
        self.fc1 = nn.Linear(2, 2)
        # 定义一个隐藏层到输出层的全连接层
        # 输入 2 个特征，输出 1 个预测值
        self.fc2 = nn.Linear(2, 1)

    def forward(self, x):
        # 前向传播过程
        # 使用 ReLU 激活函数处理隐藏层输出
        x = torch.relu(self.fc1(x))
        # 输出层
        x = self.fc2(x)
        return x
\end{verbatim}

定义好网络结构后，我需要将其实例化。通过实例化，我们就在内存中创建了一个具体的网络对象。然后，我可以打印这个模型实例，来查看它的具体结构，这对于调试和确认网络配置非常有帮助。

\textbf{代码 2：创建模型实例并打印其结构}
\begin{verbatim}
# 创建模型实例
model = SimpleNN()
# 打印模型
print(model)
\end{verbatim}

运行以上代码，会输出网络的结构信息，清晰地展示了我们定义的两个全连接层及其参数。

最后，为了验证模型是否能够正常工作，我创建了一个虚拟的输入数据（一个符合输入层要求的张量），并将其传入模型中，执行一次完整的前向传播过程，以获得模型的输出。结果如图\ref{模型实例}

\begin{figure}[htb]
	\centering
	\includegraphics[width=\linewidth]{assets/images/模型实例.png}
	\caption{模型实例}
	\label{模型实例}
\end{figure}

\textbf{代码 3：创建输入数据并执行前向传播}
\begin{verbatim}
# 创建一个随机的输入张量，大小为 (batch_size, input_features)
# 这里假设 batch_size=1, input_features=2
input_tensor = torch.randn(1, 2)

# 将输入数据传入模型，获得预测结果
output = model(input_tensor)

print(f"Input Tensor: {input_tensor}")
print(f"Output Prediction: {output}")
\end{verbatim}
结果如图\ref{向前传播}
\begin{figure}[H]
	\centering
	\includegraphics[width=\linewidth]{assets/images/向前传播.png}
	\caption{向前传播}
	\label{向前传播}
\end{figure}

\subsection{构建并训练我的第一个神经网络}

在掌握了如何定义神经网络模型的基础后，我将注意力转向了如何\textbf{训练}一个完整的网络。一个完整的训练过程通常包含以下几个核心步骤：
\begin{enumerate}
    \item \textbf{准备数据}：定义输入特征和对应的目标标签。
    \item \textbf{定义模型}：构建神经网络结构。
    \item \textbf{定义损失函数}：选择一个标准来衡量模型预测值与真实值之间的差距。
    \item \textbf{定义优化器}：选择一种算法来根据损失更新模型的权重。
    \item \textbf{迭代训练}：循环执行前向传播、计算损失、反向传播和参数更新。
\end{enumerate}

我的第一个实践是完成一个简单的二分类任务。我使用 \texttt{nn.Sequential} 快速搭建了一个包含一个隐藏层的网络。损失函数我选用了均方误差损失 \texttt{nn.MSELoss}，优化器则选择了随机梯度下降 \texttt{torch.optim.SGD}。

下面的代码整合了从数据创建到模型训练，再到最后将训练过程中的损失值进行可视化的完整流程。通过绘制损失曲线，我可以直观地判断模型是否在有效地学习。

\textbf{代码 1：完整的模型训练与损失可视化}
\begin{verbatim}
import torch
import torch.nn as nn
import matplotlib.pyplot as plt

# 1. 定义输入层大小、隐藏层大小、输出层大小和批量大小
n_in, n_h, n_out, batch_size = 10, 5, 1, 10

# 2. 创建虚拟输入数据和目标数据
x = torch.randn(batch_size, n_in)
y = torch.tensor([[1.0], [0.0], [0.0], [1.0], [1.0], [1.0], [0.0], [0.0], [1.0], [1.0]])

# 3. 定义模型：使用 nn.Sequential 快速搭建
model = nn.Sequential(
    nn.Linear(n_in, n_h),      # 输入层到隐藏层的线性变换
    nn.ReLU(),                 # 隐藏层的ReLU激活函数
    nn.Linear(n_h, n_out),     # 隐藏层到输出层的线性变换
    nn.Sigmoid()               # 输出层的Sigmoid激活函数
)

# 4. 定义损失函数和优化器
criterion = nn.MSELoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.01) # 学习率为0.01

# 5. 迭代训练并记录损失
losses = []
epochs = 50
for epoch in range(epochs):
    # 前向传播：计算预测值
    y_pred = model(x)
    # 计算损失
    loss = criterion(y_pred, y)
    losses.append(loss.item()) # 记录损失值
    
    print(f'Epoch [{epoch+1}/{epochs}], Loss: {loss.item():.4f}')
    
    # 反向传播和优化
    optimizer.zero_grad() # 清空梯度
    loss.backward()       # 计算梯度
    optimizer.step()      # 更新权重

# 6. 可视化损失变化曲线
plt.figure(figsize=(8, 5))
plt.plot(range(1, epochs + 1), losses, label='Loss')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.title('Training Loss Over Epochs')
plt.legend()
plt.grid(True)
plt.show()
\end{verbatim}



为了更深入地理解神经网络如何对数据进行分类，我进行了第二个实验。这次，我创建了一个二维数据集，其中数据点根据其是否在一个圆形区域内被分为两类。这种数据可以被直观地展示在二维平面上。


\textbf{代码 2：生成并可视化二维分类数据集}
\begin{verbatim}
import torch
import matplotlib.pyplot as plt

# 生成一些随机数据
n_samples = 100
# 生成 100 个二维数据点
data = torch.randn(n_samples, 2) 
# 如果点在圆内(x^2 + y^2 < 1)，标签为1，否则为0
labels = (data[:, 0]**2 + data[:, 1]**2 < 1).float().unsqueeze(1) 

# 可视化数据
plt.figure(figsize=(8, 6))
plt.scatter(data[:, 0], data[:, 1], c=labels.squeeze(), cmap='coolwarm')
plt.title("Generated Data")
plt.xlabel("Feature 1")
plt.ylabel("Feature 2")
plt.show()
\end{verbatim}

针对这个二维数据集，我构建了一个新的神经网络，并使用二元交叉熵损失(\texttt{nn.BCELoss})进行训练。训练完成后，最关键的一步是\textbf{可视化决策边界}。决策边界展示了模型在特征空间中是如何划分不同类别的区域的。这为我们提供了一个关于模型学习效果的直观评估。

下面的最终代码整合了数据生成、模型定义、训练以及决策边界可视化的所有步骤。

\textbf{代码 3：训练二维分类模型并可视化决策边界}
\begin{verbatim}
import torch
import torch.nn as nn
import torch.optim as optim
import matplotlib.pyplot as plt
import numpy as np

# 1. 数据准备
n_samples = 100
data = torch.randn(n_samples, 2)
labels = (data[:, 0]**2 + data[:, 1]**2 < 1).float().unsqueeze(1)

# 2. 定义神经网络
class SimpleNN(nn.Module):
    def __init__(self):
        super(SimpleNN, self).__init__()
        self.fc1 = nn.Linear(2, 4) # 输入层有2个特征，隐藏层有4个神经元
        self.fc2 = nn.Linear(4, 1) # 隐藏层输出到1个神经元 (用于二分类)
    
    def forward(self, x):
        x = torch.relu(self.fc1(x))
        x = torch.sigmoid(self.fc2(x)) # 输出层使用 Sigmoid
        return x

model = SimpleNN()

# 3. 定义损失函数和优化器
criterion = nn.BCELoss()
optimizer = optim.SGD(model.parameters(), lr=0.1)

# 4. 训练模型
epochs = 100
for epoch in range(epochs):
    outputs = model(data)
    loss = criterion(outputs, labels)
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    if (epoch + 1) % 10 == 0:
        print(f'Epoch [{epoch + 1}/{epochs}], Loss: {loss.item():.4f}')

# 5. 可视化决策边界
def plot_decision_boundary(model, data, labels):
    x_min, x_max = data[:, 0].min() - 1, data[:, 0].max() + 1
    y_min, y_max = data[:, 1].min() - 1, data[:, 1].max() + 1
    xx, yy = np.meshgrid(np.arange(x_min.item(), x_max.item(), 0.1),
                         np.arange(y_min.item(), y_max.item(), 0.1))
    
    grid = torch.cat((torch.tensor(xx.ravel()).reshape(-1, 1), 
                      torch.tensor(yy.ravel()).reshape(-1, 1)), dim=1).float()
    
    predictions = model(grid).detach().numpy().reshape(xx.shape)
    
    plt.figure(figsize=(8, 6))
    plt.contourf(xx, yy, predictions, levels=[0, 0.5, 1], cmap='coolwarm', alpha=0.7)
    plt.scatter(data[:, 0], data[:, 1], c=labels.squeeze(), cmap='coolwarm', edgecolors='k')
    plt.title("Decision Boundary")
    plt.xlabel("Feature 1")
    plt.ylabel("Feature 2")
    plt.show()

plot_decision_boundary(model, data, labels)
\end{verbatim}

结果如图\ref{图1}，图\ref{Decision Boundary}

\begin{figure}[htb]
	\centering
	\includegraphics[width=\linewidth]{assets/images/image3.png}
	\caption{结果}
	\label{图1}
\end{figure}

\begin{figure}[htb]
	\centering
	\includegraphics[width=\linewidth]{assets/images/Decision Boundary.png}
	\caption{Decision Boundary}
	\label{Decision Boundary}
\end{figure}

\subsection{学习 PyTorch 数据处理与加载机制}

在完成了初步的模型搭建和训练后，我意识到一个高效、规范的数据处理流程是深度学习项目中至关重要的一环。为此，我深入学习了 PyTorch 提供的两大核心工具：\texttt{torch.utils.data.Dataset} 和 \texttt{torch.utils.data.DataLoader}。

\textbf{Dataset} 类的作用是作为一个数据集的抽象。为了能够处理我自己的数据，我学习了如何通过继承 \texttt{Dataset} 类来创建一个自定义的数据集。自定义的数据集类必须实现三个关键方法：
\begin{itemize}
    \item \texttt{\_\_init\_\_()}：构造函数，用于加载和初始化数据，例如从文件中读取数据。
    \item \texttt{\_\_len\_\_()}：返回数据集中样本的总数。
    \item \texttt{\_\_getitem\_\_(idx)}：根据给定的索引 \texttt{idx}，获取并返回一个样本（通常是一个数据和对应标签的元组）。
\end{itemize}

下面的代码展示了我如何根据一组给定的列表数据，创建一个自定义的数据集。在这个过程中，我确保了在 \texttt{\_\_getitem\_\_} 方法中将数据和标签都转换为了 PyTorch 的 Tensor 格式，这是模型训练所必需的。

\textbf{代码 1：创建自定义 Dataset 类}
\begin{verbatim}
import torch
from torch.utils.data import Dataset

# 示例数据
X_data = [[1, 2], [3, 4], [5, 6], [7, 8]]  # 输入特征
Y_data = [[1], [0], [1], [0]]             # 目标标签

# 自定义数据集类
class MyDataset(Dataset):
    def __init__(self, X_data, Y_data):
        """初始化数据集"""
        self.X_data = X_data
        self.Y_data = Y_data

    def __len__(self):
        """返回数据集的大小"""
        return len(self.X_data)

    def __getitem__(self, idx):
        """根据指定索引返回数据"""
        x = torch.tensor(self.X_data[idx], dtype=torch.float32)
        y = torch.tensor(self.Y_data[idx], dtype=torch.float32)
        return x, y

# 创建数据集实例
custom_dataset = MyDataset(X_data, Y_data)

# 测试一下
print(f"Dataset size: {len(custom_dataset)}")
print(f"First sample: {custom_dataset[0]}")
\end{verbatim}

仅仅拥有一个 \texttt{Dataset} 是不够的。在模型训练时，我们通常需要对数据进行\textbf{批量处理 (Batching)}、\textbf{随机打乱 (Shuffling)} 以及并行加载。这些功能都可以通过 \texttt{DataLoader} 来轻松实现。\texttt{DataLoader} 接收一个 \texttt{Dataset} 对象，并将其包装成一个可迭代对象，让我们可以方便地在训练循环中按批次获取数据。

在下面的代码中，我将上一步创建的 \texttt{custom\_dataset} 实例传递给 \texttt{DataLoader}，并设置了批量大小(\texttt{batch\_size})为2，同时开启了随机打乱(\texttt{shuffle=True})。

\textbf{代码 2：使用 DataLoader 批量加载数据}
\begin{verbatim}
from torch.utils.data import DataLoader

# 创建 DataLoader 实例
# batch_size=2 表示每次迭代加载2个样本
# shuffle=True 表示在每个 epoch 开始时打乱数据顺序
data_loader = DataLoader(dataset=custom_dataset, batch_size=2, shuffle=True)

# 遍历 DataLoader 来获取批量数据
print("\nIterating through DataLoader:")
for epoch in range(1): # 仅演示一个 epoch
    for batch_idx, (inputs, labels) in enumerate(data_loader):
        print(f"  Batch {batch_idx + 1}:")
        print(f"    Inputs shape: {inputs.shape}")
        print(f"    Labels shape: {labels.shape}")
\end{verbatim}

对于图像数据，除了加载外，通常还需要进行\textbf{预处理}和\textbf{数据增强}。PyTorch 的 \texttt{torchvision} 库为此提供了强大的支持。我学习了如何使用 \texttt{torchvision.transforms} 来定义一系列的数据变换操作，例如将图像转换为 Tensor (\texttt{ToTensor})和标准化(\texttt{Normalize})。

更进一步，我学习了如何直接使用 \texttt{torchvision.datasets} 来加载像 MNIST 这样的标准数据集。这极大地简化了数据准备工作，因为下载、加载和应用转换的整个流程都被封装好了。下面的代码演示了加载 MNIST 训练集、定义转换操作，并用 \texttt{DataLoader} 进行封装的完整过程。

\textbf{代码 3：使用 torchvision 加载并处理 MNIST 数据集}
\begin{verbatim}
import torchvision
import torchvision.transforms as transforms
from torch.utils.data import DataLoader

# 定义一个转换流程：先转为 Tensor，再进行标准化
transform = transforms.Compose([
    transforms.ToTensor(), # 将 PIL Image 或 numpy.ndarray 转换为 tensor (C, H, W)，并缩放到 [0.0, 1.0]
    transforms.Normalize((0.5,), (0.5,)) # 对灰度图进行标准化
])

# 下载并加载 MNIST 训练数据集
train_dataset = torchvision.datasets.MNIST(
    root='./data',        # 数据存放路径
    train=True,           # 加载训练集
    transform=transform,  # 应用我们定义的转换
    download=True         # 如果本地没有，则自动下载
)

# 使用 DataLoader 进行封装
train_loader = DataLoader(
    dataset=train_dataset,
    batch_size=64,        # 设置批量大小为 64
    shuffle=True          # 打乱数据
)

# 打印一个批次的数据形状以作验证
print("\nMNIST DataLoader:")
# 获取一个批次的数据
inputs, labels = next(iter(train_loader))
print(f"Shape of one batch of inputs (images): {inputs.shape}")
print(f"Shape of one batch of labels: {labels.shape}")
\end{verbatim}

结果如图\ref{ 数据处理与加载机制}
\begin{figure}[htb]
	\centering
	\includegraphics[width=\linewidth]{assets/images/ 数据处理与加载机制.png}
	\caption{ 数据处理与加载机制}
	\label{ 数据处理与加载机制}
\end{figure}

\subsection{应用卷积神经网络 (CNN) 完成图像分类}

在我掌握了基本神经网络和数据加载流程后，我决定挑战一个更专业的图像处理模型——\textbf{卷积神经网络（Convolutional Neural Network, CNN）}。我了解到，CNN 通过其核心的卷积层和池化层，能够有效地从图像中提取空间层次特征（如边缘、纹理等），因此在图像分类等计算机视觉任务中表现卓越。

一个典型的 CNN 结构主要由以下几个部分组成：
\begin{itemize}
    \item \textbf{卷积层 (Convolutional Layer)}：使用滤波器（或称为核）在输入图像上滑动，以提取局部特征。在 PyTorch 中通过 \texttt{nn.Conv2d} 实现。
    \item \textbf{池化层 (Pooling Layer)}：对卷积层输出的特征图进行降采样，以减少计算量和参数数量，同时保留最重要的特征。最常用的是最大池化 (\texttt{nn.MaxPool2d})。
    \item \textbf{全连接层 (Fully Connected Layer)}：在网络的末端，将前面提取到的高级特征进行整合，并映射到最终的类别输出。这与我们之前学习的标准神经网络层 (\texttt{nn.Linear}) 相同。
\end{itemize}

本次实验的目标是使用 PyTorch 构建一个 CNN 模型，对 MNIST 手写数字数据集进行分类。为此，我设计了一个包含两个卷积层、两个最大池化层和两个全连接层的网络。对于这个10分类任务，我选用了更适合的\textbf{交叉熵损失函数 (\texttt{nn.CrossEntropyLoss})}。

下面的代码整合了从数据加载、CNN 模型定义、模型训练、在测试集上评估准确率，到最后可视化部分预测结果的全部流程。

\textbf{代码 1：构建、训练并评估一个完整的 CNN 模型}
\begin{verbatim}
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets, transforms
import matplotlib.pyplot as plt

# 1. 数据加载与预处理
transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.5,), (0.5,)) # 归一化到 [-1, 1]
])

train_dataset = datasets.MNIST(root='./data', train=True, transform=transform, download=True)
test_dataset = datasets.MNIST(root='./data', train=False, transform=transform, download=True)

train_loader = torch.utils.data.DataLoader(dataset=train_dataset, batch_size=64, shuffle=True)
test_loader = torch.utils.data.DataLoader(dataset=test_dataset, batch_size=64, shuffle=False)

# 2. 定义 CNN 模型
class SimpleCNN(nn.Module):
    def __init__(self):
        super(SimpleCNN, self).__init__()
        # 卷积层1: 输入1通道(灰度图), 输出32通道, 卷积核3x3, padding=1
        self.conv1 = nn.Conv2d(1, 32, kernel_size=3, stride=1, padding=1)
        # 卷积层2: 输入32通道, 输出64通道, 卷积核3x3, padding=1
        self.conv2 = nn.Conv2d(32, 64, kernel_size=3, stride=1, padding=1)
        # 全连接层1: 输入 64 * 7 * 7, 输出 128
        self.fc1 = nn.Linear(64 * 7 * 7, 128)
        # 全连接层2: 输出 10个类别
        self.fc2 = nn.Linear(128, 10)

    def forward(self, x):
        # x 初始形状: [batch_size, 1, 28, 28]
        x = F.relu(self.conv1(x))
        x = F.max_pool2d(x, 2) # 形状变为 [batch, 32, 14, 14]
        x = F.relu(self.conv2(x))
        x = F.max_pool2d(x, 2) # 形状变为 [batch, 64, 7, 7]
        x = x.view(-1, 64 * 7 * 7) # 展平操作
        x = F.relu(self.fc1(x))
        x = self.fc2(x)
        return x

model = SimpleCNN()

# 3. 定义损失函数与优化器
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9)

# 4. 模型训练
num_epochs = 5
model.train() # 设置为训练模式
for epoch in range(num_epochs):
    total_loss = 0
    for images, labels in train_loader:
        outputs = model(images)
        loss = criterion(outputs, labels)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        total_loss += loss.item()
    print(f"Epoch [{epoch+1}/{num_epochs}], Loss: {total_loss / len(train_loader):.4f}")

# 5. 模型测试
model.eval() # 设置为评估模式
correct = 0
total = 0
with torch.no_grad(): # 评估时不需要计算梯度
    for images, labels in test_loader:
        outputs = model(images)
        _, predicted = torch.max(outputs.data, 1)
        total += labels.size(0)
        correct += (predicted == labels).sum().item()
print(f"Test Accuracy: {100 * correct / total:.2f}%")

# 6. 可视化测试结果
dataiter = iter(test_loader)
images, labels = next(dataiter)
outputs = model(images)
_, predictions = torch.max(outputs, 1)

fig, axes = plt.subplots(1, 6, figsize=(12, 4))
for i in range(6):
    axes[i].imshow(images[i][0], cmap='gray')
    axes[i].set_title(f"Label: {labels[i]}\nPred: {predictions[i]}")
    axes[i].axis('off')
plt.show()
\end{verbatim}

结果如图
\begin{figure}[htb]
	\centering
	\includegraphics[width=\linewidth]{assets/images/卷神经网络.png}
	\caption{卷神经网络}
	\label{卷积神经网络}
\end{figure}

\subsection{数据集的高级应用：从 CSV 文件加载数据}

在之前的学习中，我已经掌握了如何使用 PyTorch 内置的数据集以及如何从内存中的数据创建自定义 \texttt{Dataset}。然而，在真实世界的项目中，数据通常存储在文件中，例如 CSV、JSON 或数据库。因此，我本次学习的重点是，如何构建一个能够直接从 CSV 文件中读取数据并进行处理的 \texttt{Dataset}。

我了解到，要实现这一目标，通常需要借助像 \textbf{Pandas} 这样的库来高效地读取和处理 CSV 文件。整个流程可以分解为：
\begin{enumerate}
    \item 在 \texttt{\_\_init\_\_} 方法中，使用 Pandas 的 \texttt{read\_csv} 函数一次性将整个文件读入内存。
    \item 在 \texttt{\_\_len\_\_} 方法中，返回数据框 (DataFrame) 的行数。
    \item 在 \texttt{\_\_getitem\_\_} 方法中，使用索引从数据框中定位到特定行，然后将其分割为特征 (features) 和标签 (label)，并最终将它们转换为 PyTorch Tensors。
\end{enumerate}

为了实践这个流程，我首先创建了一个名为 \texttt{runoob\_pytorch\_data.csv} 的数据文件，它包含几列特征和一列标签。

\textbf{数据文件: \texttt{runoob\_pytorch\_data.csv}}
\begin{verbatim}
feature1,feature2,feature3,label
0.5,-1.2,3.3,1
-0.3,0.8,1.2,0
2.1,-3.3,0.0,1
1.5,2.2,-1.1,0
-0.8,0.0,0.3,1
1.0,1.1,-2.0,0
-1.1,0.8,1.5,1
0.9,-0.5,-1.8,0
1.2,2.1,-3.0,1
-2.3,0.4,0.7,0
\end{verbatim}

接着，我编写了下面的 Python 代码。这段代码定义了一个 \texttt{CSVDataset} 类，它继承自 \texttt{torch.utils.data.Dataset}，并实现了从上述 CSV 文件加载数据的完整逻辑。

\textbf{代码 1：定义一个可以从 CSV 文件加载数据的 Dataset}
\begin{verbatim}
import torch
import pandas as pd
from torch.utils.data import Dataset, DataLoader

# 自定义数据集类
class CSVDataset(Dataset):
    def __init__(self, file_path):
        # 使用 pandas 读取 csv 文件
        self.data = pd.read_csv(file_path)

    def __len__(self):
        # 返回数据的大小
        return len(self.data)

    def __getitem__(self, idx):
        # 使用 .iloc 明确基于位置索引
        row = self.data.iloc[idx]
        # 将特征和标签分开
        # 特征是除了最后一列之外的所有列
        features = torch.tensor(row.iloc[:-1].to_numpy(), dtype=torch.float32)
        # 标签是最后一列
        label = torch.tensor(row.iloc[-1], dtype=torch.float32)
        return features, label

# 实例化数据集
csv_dataset = CSVDataset("runoob_pytorch_data.csv")
print(f"Successfully loaded dataset from CSV.")
print(f"Total samples in dataset: {len(csv_dataset)}")
# 测试一下第一个样本
features, label = csv_dataset[0]
print(f"Features of first sample: {features}")
print(f"Label of first sample: {label}")
\end{verbatim}

成功定义并测试了 \texttt{CSVDataset} 后，最后一步就是将它与 \texttt{DataLoader} 结合起来，以实现高效的批量加载。这与之前处理其他 \texttt{Dataset} 对象的方式完全相同，充分展示了 PyTorch 数据加载工具的模块化和通用性。

下面的代码实例化了 \texttt{CSVDataset} 和 \texttt{DataLoader}，并遍历输出了第一个数据批次的内容，以验证整个数据加载管道是否正常工作。

\textbf{代码 2：使用 DataLoader 封装自定义的 CSVDataset}
\begin{verbatim}
# 实例化数据集和 DataLoader
dataset = CSVDataset("runoob_pytorch_data.csv")
dataloader = DataLoader(dataset, batch_size=4, shuffle=True)

# 遍历 DataLoader
print("\n--- Iterating through the DataLoader ---")
# 获取一个批次的数据
features_batch, labels_batch = next(iter(dataloader))

print(f"Shape of features batch: {features_batch.shape}")
print("Features in the first batch:")
print(features_batch)
print("\nShape of labels batch: {labels_batch.shape}")
print("Labels in the first batch:")
print(labels_batch)
\end{verbatim}

结果如图\ref{数据集}
\begin{figure}[htb]
	\centering
	\includegraphics[width=\linewidth]{assets/images/数据集.png}
	\caption{数据集}
	\label{数据集}
\end{figure}

\subsection{掌握数据转换与增强技术}

在构建了完整的数据加载和模型训练流程后，我将学习的重点放在了如何进一步优化数据输入环节上。我了解到，原始数据往往不能直接送入模型，需要经过一系列的\textbf{数据转换 (Data Transformation)}。在 PyTorch 中，这项任务主要通过 \texttt{torchvision.transforms} 模块来完成。

数据转换主要服务于两大目的：
\begin{itemize}
    \item \textbf{数据预处理}：确保数据符合模型的输入要求。这包括将数据转换为 Tensor 格式 (\texttt{ToTensor})、调整图像尺寸 (\texttt{Resize}) 以及标准化数据 (\texttt{Normalize}) 等。这些是保证模型能够正常训练的基础步骤。
    \item \textbf{数据增强 (Data Augmentation)}：在训练过程中，对图像进行随机变换（如随机旋转、翻转、裁剪等），从而人为地增加训练数据的多样性。这是一种非常有效的正则化技术，可以显著提升模型的泛化能力，避免过拟合。
\end{itemize}

为了将多个转换操作有序地应用到数据上，我学习了使用 \texttt{transforms.Compose}。它能够将一个转换列表组合成一个统一的流水线 (pipeline)，代码结构清晰且易于管理。

下面的代码演示了一个基础的数据预处理流程，其中包含了尺寸调整、Tensor 转换和标准化。我将其应用于 MNIST 数据集，并打印出一个批次数据的张量形状，以验证尺寸调整是否生效。

\textbf{代码 1：对数据集应用基础的预处理转换}
\begin{verbatim}
from torchvision import datasets, transforms
from torch.utils.data import DataLoader

# 定义一个预处理的转换流水线
transform_preprocess = transforms.Compose([
    transforms.Resize((128, 128)),     # 将图像大小调整为 128x128
    transforms.ToTensor(),             # 转换为 Tensor，并归一化到 [0, 1]
    transforms.Normalize((0.5,), (0.5,)) # 标准化到 [-1, 1]
])

# 加载 MNIST 数据集，并应用定义的转换
train_dataset = datasets.MNIST(
    root='./data', 
    train=True, 
    transform=transform_preprocess, 
    download=True
)

# 使用 DataLoader 加载数据
train_loader = DataLoader(dataset=train_dataset, batch_size=32, shuffle=True)

# 检查转换后的数据形状
images, labels = next(iter(train_loader))
print("--- After Preprocessing Transform ---")
print(f"Shape of one batch of images: {images.shape}")
print(" -> The image size is now 128x128 as expected.")
\end{verbatim}

在掌握了预处理之后，我进一步探索了数据增强技术。我定义了一个包含随机水平翻转和随机旋转的增强流水线。为了直观地感受数据增强的效果，我编写了代码来加载原始图像，并排展示经过随机增强变换后的版本。通过对比，可以清晰地看到每次加载同一张图片时，它都会呈现出不同的形态。

\textbf{代码 2：定义数据增强转换并可视化其效果}
\begin{verbatim}
import matplotlib.pyplot as plt
from torchvision import datasets, transforms

# 定义一个包含数据增强的转换流水线
transform_augment = transforms.Compose([
    transforms.RandomHorizontalFlip(p=0.5), # 50% 的概率水平翻转
    transforms.RandomRotation(30),          # 在 (-30, 30) 度之间随机旋转
    transforms.ToTensor()                   # 转换为 Tensor
])

# 加载数据集并应用增强转换
augmented_dataset = datasets.MNIST(
    root='./data', 
    train=True, 
    transform=transform_augment, 
    download=True
)

# 定义一个函数来显示图像
def show_augmented_images(dataset, num_images=5):
    fig, axs = plt.subplots(1, num_images, figsize=(15, 5))
    fig.suptitle('Images After Data Augmentation', fontsize=16)
    for i in range(num_images):
        # 每次从数据集中获取索引为 i 的样本，都会应用一次新的随机变换
        image, label = dataset[i]
        # imshow 需要 (H, W) 或 (H, W, C) 格式，我们用 squeeze() 去掉颜色通道维度
        axs[i].imshow(image.squeeze(), cmap='gray')
        axs[i].set_title(f"Original Label: {label}")
        axs[i].axis('off')
    plt.show()

# 调用函数显示增强后的图像
show_augmented_images(augmented_dataset)
\end{verbatim}

结果如图
\begin{figure}[htb]
	\centering
	\includegraphics[width=\linewidth]{assets/images/数据转换.png}
	\caption{数据转换}
	\label{数据转换}
\end{figure}

\subsection{综合项目：CIFAR-10 图像分类实践}

作为本次 PyTorch 学习的收官项目，我选择了一个比 MNIST 更具挑战性的图像分类任务——CIFAR-10。该数据集包含10个类别的32x32彩色图像，这要求模型具备更强的特征提取能力。本次实验的目标是走通一个工业级深度学习项目的全流程，包括：数据准备、模型构建、训练、评估、保存及加载应用。

\textbf{1. 环境准备与数据加载}

首先，我使用了 \texttt{torchvision} 来加载 CIFAR-10 数据集。与 MNIST 不同，CIFAR-10 是三通道的彩色图像，因此在进行标准化 (\texttt{Normalize}) 时，需要分别为 R, G, B 三个通道提供均值和标准差。

\textbf{2. 构建卷积神经网络 (CNN)}

我设计了一个经典的 LeNet-style 卷积神经网络。该网络包含两个卷积层和三个全连接层，并通过最大池化层进行下采样。我特别注意了网络各层之间张量维度的变化，确保数据流能够顺畅地从 32x32x3 的输入图像传递到最终的10分类输出。

\textbf{3. 模型训练与评估}

我选用了交叉熵损失函数 (\texttt{nn.CrossEntropyLoss}) 和带动量的随机梯度下降优化器 (\texttt{optim.SGD})。在训练过程中，我循环迭代了10个 epoch，并在每个 epoch 结束后，在测试集上评估模型的整体准确率。此外，我还进一步分析了模型在每一个具体类别上的表现，这有助于发现模型的短板。

\textbf{4. 模型保存、加载与应用}

训练完成后，我学习了如何使用 \texttt{torch.save()} 来保存模型的权重参数 (\texttt{state\_dict})。这是一个至关重要的步骤，它使得我们可以在不重新训练的情况下，随时复用或部署模型。接着，我演示了如何重新实例化网络结构，并使用 \texttt{torch.load()} 和 \texttt{load\_state\_dict()} 来加载已保存的权重，最后用加载好的模型对新的测试图像进行预测。

下面的代码并非项目的完整脚本，而是将整个流程中最核心的部分——从模型定义到最终预测——整合在了一起，集中展示了本次综合实践的成果。

\textbf{代码 1：CIFAR-10 图像分类全流程核心实现}
\begin{verbatim}
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
import numpy as np

# 1. 定义 CNN 模型
class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(3, 6, 5) # 输入3通道, 输出6通道, 5x5卷积核
        self.pool = nn.MaxPool2d(2, 2)
        self.conv2 = nn.Conv2d(6, 16, 5)
        self.fc1 = nn.Linear(16 * 5 * 5, 120)
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, 10)

    def forward(self, x):
        x = self.pool(F.relu(self.conv1(x)))
        x = self.pool(F.relu(self.conv2(x)))
        x = x.view(-1, 16 * 5 * 5) # 展平
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x

# 为了脚本能独立运行，我们在此处包含数据加载和类别定义
transform = transforms.Compose(
    [transforms.ToTensor(),
     transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])

testset = torchvision.datasets.CIFAR10(root='./data', train=False,
                                       download=True, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=4,
                                         shuffle=False, num_workers=0)
classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

# 2. 实例化模型、损失函数和优化器 (此处仅为结构展示)
net = Net()
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)
# --- 假设模型已经训练完毕 ---

# 3. 模型保存与加载
# 保存模型 (在真实训练后执行)
# PATH = './cifar_net.pth'
# torch.save(net.state_dict(), PATH)

# 加载模型
# 注意：你需要先运行一次完整的训练脚本来生成 cifar_net.pth 文件
# 或者, 我们直接使用随机初始化的模型进行预测流程演示
print("--- Model Application Demonstration ---")
# 实例化一个新的网络
loaded_net = Net() 
# loaded_net.load_state_dict(torch.load(PATH)) # 从文件加载权重

# 4. 使用加载(或实例化)的模型进行预测
dataiter = iter(testloader)
images, labels = next(dataiter)

# 打印原始图像和标签
def imshow(img):
    img = img / 2 + 0.5     # unnormalize
    npimg = img.numpy()
    plt.imshow(np.transpose(npimg, (1, 2, 0)))
    plt.show()

print("GroundTruth: ", ' '.join(f'{classes[labels[j]]:5s}' for j in range(4)))
# imshow(torchvision.utils.make_grid(images)) # 取消注释以显示图像

# 进行预测
outputs = loaded_net(images)
_, predicted = torch.max(outputs, 1)

print('Predicted:   ', ' '.join(f'{classes[predicted[j]]:5s}' for j in range(4)))
print("\nNote: Predictions are random as the model is not trained in this script.")
\end{verbatim}

结果如图\ref{综合项目}
\begin{figure}[H]
	\centering
	\includegraphics[width=\linewidth]{assets/images/综合项目.png}
	\caption{综合项目}
	\label{综合项目}
\end{figure}